Serverless Application ModelのCodeUriプロパティとデプロイメントパッケージの関係を理解する
はじめに
こんにちは、中山です。
最近Serverless Application Model(以下AWS SAM)の動作を検証しています。このモデルではLambda関数をAWS::Serverless::Functionリソースで定義可能です。このリソースで定義可能な CodeUri
プロパティの挙動がいまいち理解できていなかったので、本エントリでまとめたいと思います。結論を先に書くと このプロパティはアーティファクトをS3にアップロードする前の段階でAWS SAM用テンプレートに定義し、Lambda関数のコードはディレクトリを分けて管理した方がよい と考えています。以下で詳しく解説します。
なお、本エントリを執筆する上で検証に利用した主要な各種ツールのバージョンは以下の通りです。バージョンによって結果が変更される可能性があるので、その点ご了承ください。
- AWS SAM: 2016-10-31
- AWS CLI: aws-cli/1.11.47 Python/2.7.12 Darwin/16.4.0 botocore/1.5.10
解説
まずはじめにAWS SAM利用時のデプロイフローをご紹介します。基本的に以下のフローになります。
- AWS SAM用CloudFormationテンプレートを作成
- Lambda関数などを作成
aws cloudformation package
コマンドで各種アーティファクト(Lambda関数のデプロイメントパッケージなど)をS3にアップロードaws cloudformation deploy
コマンドでCloudFormationスタックを作成/更新
ステップ1で作成したテンプレートはステップ3実行時にAWS SAM用テンプレートへ変換されます。変換後のテンプレートはデフォルトで標準出力に表示されますが、 --output-template-file <file>
オプションでファイルに出力することが可能です。例えば以下のようなテンプレートがあったとします。
sam.yml
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Test Resources: Func1: Type: AWS::Serverless::Function Properties: Handler: handler.hello Runtime: python2.7
このファイルを以下のコマンドで変換(と同時にアップロード)してみます。 <_YOUR_S3_BUCKET_>
はご自身の環境に置き換えてください。
$ aws cloudformation package \ --template-file sam.yml \ --s3-bucket <_YOUR_S3_BUCKET_> \ --output-template-file packaged.yml
変換後のファイルは以下のようになります。
packaged.yml
AWSTemplateFormatVersion: 2010-09-09 Description: Test Resources: Func1: Properties: CodeUri: s3://<_YOUR_S3_BUCKET_>/0dc56727e0ba0463637fdeea23edc24c Handler: handler.hello Runtime: python2.7 Type: AWS::Serverless::Function Transform: AWS::Serverless-2016-10-31
微妙に細かい部分が変わっていますが、一番大きな変化は AWS::Serverless::Function
リソースに CodeUri
プロパティが追加されていることです。先程リンクしたドキュメントに記載されているように、このプロパティはLambda関数のデプロイメントパッケージへのパスを指定します。該当の部分を引用します。
Property Name | Type | Description |
---|---|---|
CodeUri | string | Required. S3 Uri to the function code. The S3 object this Uri references MUST be a Lambda deployment package. |
ただし、S3に保存されたデプロイメントパッケージを確認すると分かりますが、Lambda関数のコードのみアップロードされるわけではない という点に注意してください。今回の例では中身が以下のようになっています。
$ aws s3 cp s3://<_YOUR_S3_BUCKET_>/0dc56727e0ba0463637fdeea23edc24c - | bsdtar -tvf - -rwxrwxrwx 0 0 0 54 Feb 12 14:21 handler.py -rwxrwxrwx 0 0 0 225 Feb 12 14:25 sam.yml
デプロイメントパッケージに本来不要である sam.yml
が含まれてしまっています。後述しますが、アップロードされるファイルは CodeUri
プロパティを指定しない場合、 aws cloudformation package
コマンドを実行したカレントディレクトリ以下のファイルになります。この挙動はどういった場合に問題になるのでしょうか。それは、 Lambda関数のコードに変更がないにも関わらず更新されてしまう場合がある という点です。例えば sam.yml
を以下のように修正したとします。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Test Resources: Func1: Type: AWS::Serverless::Function Properties: Handler: handler.hello Runtime: python2.7 Outputs: Func1Name: Value: !Ref Func1
アウトプットでLambda関数名を表示させているだけです。この修正後、テンプレートを変換すると以下のようにこのプロパティの値が書き換わることが確認できます。
AWSTemplateFormatVersion: 2010-09-09 Description: Test Outputs: Func1Name: Value: Ref: Func1 Resources: Func1: Properties: CodeUri: s3://<_YOUR_S3_BUCKET_>/ce0dac5704bc958cb1e79046ccfab1b3 Handler: handler.hello Runtime: python2.7 Type: AWS::Serverless::Function Transform: AWS::Serverless-2016-10-31
当然ですが、この状態で aws cloudformation deploy
を実行すると、デプロイメントパッケージの内容が変更されたと判断されるので、以下のようにLambda関数が更新されます。
$ aws cloudformation describe-stack-events \ --stack-name test { "StackEvents": [ { "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "EventId": "12cd83c0-f0ee-11e6-add6-50a68668d04a", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2017-02-12T06:39:58.913Z", "StackName": "test", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "LogicalResourceId": "test" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "EventId": "11faf180-f0ee-11e6-add6-50a68668d04a", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2017-02-12T06:39:57.518Z", "StackName": "test", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "LogicalResourceId": "test" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "EventId": "Func1-UPDATE_COMPLETE-2017-02-12T06:39:54.441Z", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::Lambda::Function", "Timestamp": "2017-02-12T06:39:54.441Z", "StackName": "test", "ResourceProperties": "{\"Role\":\"arn:aws:iam::************:role/test-Func1Role-1SA58HG9XM3BI\",\"Runtime\":\"python2.7\",\"Handler\":\"handler.hello\",\"Code\":{\"S3Bucket\":\"<_YOUR_S3_BUCKET_>\",\"S3Key\":\"ce0dac5704bc958cb1e79046ccfab1b3\"}}\n", "PhysicalResourceId": "test-Func1-LRSDPIWVG2J8", "LogicalResourceId": "Func1" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "EventId": "Func1-UPDATE_IN_PROGRESS-2017-02-12T06:39:53.823Z", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::Lambda::Function", "Timestamp": "2017-02-12T06:39:53.823Z", "StackName": "test", "ResourceProperties": "{\"Role\":\"arn:aws:iam::************:role/test-Func1Role-1SA58HG9XM3BI\",\"Runtime\":\"python2.7\",\"Handler\":\"handler.hello\",\"Code\":{\"S3Bucket\":\"<_YOUR_S3_BUCKET_>\",\"S3Key\":\"ce0dac5704bc958cb1e79046ccfab1b3\"}}\n", "PhysicalResourceId": "test-Func1-LRSDPIWVG2J8", "LogicalResourceId": "Func1" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "EventId": "0c6a8910-f0ee-11e6-b5ad-50d5ca9ff42a", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2017-02-12T06:39:48.163Z", "ResourceStatusReason": "User Initiated", "StackName": "test", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "LogicalResourceId": "test" }, <snip>
Lambda関数が更新された場合、どういった挙動になるのか検証する必要がありますが、多くの方にとってこの動作は望ましくないはずです。コードに変更がないのであれば、更新処理も実施して欲しくありません。また、スタックの状態が完了に遷移するまで時間が伸びる点も懸念されます。この問題に対する対処方法としては、冒頭にも記載したように以下2つの方法が考えられます。
CodeUri
プロパティへデプロイメントパッケージに含ませたいLambda関数への相対パスを指定する- Lambda関数毎にディレクトリを分ける
1点目について。ドキュメントを見ると一件 CodeUri
プロパティにはS3のパスしか指定できないように読めますが、これはあくまでスタックを作成/更新する時( aws cloudformation deploy
を実行する時)の話です。テンプレートの変換前に指定した CodeUri
プロパティは、変換後デプロイメントパッケージへのパスに書き換えてくれます。 aws cloudformation package
のヘルプを見るとこのプロパティにローカルファイルへのパスが指定できると記載されています。少し長いですが該当の部分を引用します。
this command can upload local artifacts specified by following properties of a resource:
o BodyS3Location property for the AWS::ApiGateway::RestApi resource
o Code property for the AWS::Lambda::Function resource
o CodeUri property for the AWS::Serverless::Function resource
o DefinitionUri property for the AWS::Serverless::Api resource
o SourceBundle property for the AWS::ElasticBeanstalk::Application Version resource
o TemplateURL property for the AWS::CloudFormation::Stack resource
to specify a local artifact in your template, specify a path to a local file or folder, as either an absolute or relative path. The relative path is a location that is relative to your template's location.
for example, if your AWS Lambda function source code is in the /home/user/code/lambdafunction/ folder, specify CodeUri: /home/user/code/lambdafunction for the AWS::Serverless::Function resource. The command returns a template and replaces the local path with the S3 location: CodeUri: s3://mybucket/lambdafunction.zip.
if you specify a file, the command directly uploads it to the S3 bucket. If you specify a folder, the command zips the folder and then uploads the .zip file. For most resources, if you don't specify a path, the command zips and uploads the current working directory. The exception is AWS::ApiGateway::RestApi; if you don't specify a bodyS3Location, this command will not upload an artifact to S3.
少し話がそれますが、直接ファイルを指定するかディレクトリを指定するかどちらの方がよいのでしょうか。私は基本的にディレクトリの方がよいと思っています。非標準の外部モジュールをデプロイメントパッケージに含ませたい場合にそちらの方が都合がいいからです。例えばLambda関数と同じディレクトリ上に vendored
というディレクトリを作成し、そこに外部モジュールを設置しておけばLambda関数から簡単に呼び出し可能です。Pythonの例ですが、以下のようにインポートできます。
import sys import os sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'vendored')) import some-external-module
2点目について。例えばディレクトリ構成が以下のようになっているとします。
$ tree . . ├── dir1 │ └── test1.txt ├── dir2 │ └── test2.txt ├── handler.py └── sam.yml 2 directories, 4 files
この状態でいくら CodeUri
プロパティを指定しても(というか指定するとしたら CodeUri: ./
になってこの用途には意味ないですが)、カレントディレクトリ以下がデプロイメントパッケージに含まれてしまうので、余計なファイルが入ります。ディレクトリの分け方はいろいろと考えられますが、例えば以下のようにLambda関数毎にディレクトリを分けたとします。
$ tree . . ├── sam.yml └── src └── handlers └── func1 └── handler.py 3 directories, 2 files
この状態でテンプレートを以下のようにしておけば、特定のファイルのみデプロイメントパッケージに含ませることが可能です。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Test Resources: Func1: Type: AWS::Serverless::Function Properties: CodeUri: src/handlers/func1 Handler: handler.hello Runtime: python2.7 Outputs: Func1Name: Value: !Ref Func1
aws cloudformation package
実行後、S3にアップロードされたデプロイメントパッケージを確認すると期待した動作になっていることが確認できます。
$ aws s3 cp s3://<_YOUR_S3_BUCKET_>/a88a4814bf124443b1fd4617581e025f - | bsdtar -tvf - -rwxrwxrwx 0 0 0 54 Feb 12 16:43 handler.py
デプロイメントパッケージに含まれるファイル以外で各種ファイルを修正しても、変換されたテンプレートの CodeUri
プロパティは変更されません。
AWSTemplateFormatVersion: 2010-09-09 Description: Test Resources: Func1: Properties: CodeUri: s3://<_YOUR_S3_BUCKET_>/a88a4814bf124443b1fd4617581e025f Handler: handler.hello Runtime: python2.7 Type: AWS::Serverless::Function Transform: AWS::Serverless-2016-10-31
当然 aws cloudformation deploy
でスタックを更新しても、デプロイメントパッケージは変更されてないのでLambda関数も更新されません。
$ aws cloudformation describe-stack-events \ --stack-name test { "StackEvents": [ { "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "EventId": "bf114ff0-f0f7-11e6-a153-50a68656dc62", "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2017-02-12T07:49:13.385Z", "StackName": "test", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "LogicalResourceId": "test" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "EventId": "be3f59f0-f0f7-11e6-aa77-50a6866998c6", "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2017-02-12T07:49:12.011Z", "StackName": "test", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "LogicalResourceId": "test" }, { "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "EventId": "ba6e54c0-f0f7-11e6-97e5-50d5ca9ff462", "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::CloudFormation::Stack", "Timestamp": "2017-02-12T07:49:05.597Z", "ResourceStatusReason": "User Initiated", "StackName": "test", "PhysicalResourceId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/3d6ca860-f006-11e6-af5b-500c44f24ce6", "LogicalResourceId": "test" }, <snip>
複数のLambda関数を定義した場合どうなるのか
解説した方法であれば対応可能です。例えば以下のような複数のLambda関数を定義したテンプレートがあったとします。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Test Resources: Func1: Type: AWS::Serverless::Function Properties: CodeUri: src/handlers/func1 Handler: func1.hello Runtime: python2.7 Func2: Type: AWS::Serverless::Function Properties: CodeUri: src/handlers/func2 Handler: func2.hello Runtime: python2.7
ディレクトリは以下の構成です。
$ tree . . ├── sam.yml └── src └── handlers ├── func1 │ └── func1.py └── func2 └── func2.py 4 directories, 3 files
この状態で func2.py
を更新後、テンプレートを変換してみます。すると以下のように更新したLambda関数のみ CodeUri
プロパティが変更されていること(関数毎にプロパティの値が変わっていること)が確認できます。
AWSTemplateFormatVersion: 2010-09-09 Description: Test Resources: Func1: Properties: CodeUri: s3://<_YOUR_S3_BUCKET_>/bfd5cbc1d484c4901175e3d180159bfe Handler: func1.hello Runtime: python2.7 Type: AWS::Serverless::Function Func2: Properties: CodeUri: s3://<_YOUR_S3_BUCKET_>/3e0ecc95dc3ada08a3d0bfbbe72aebab Handler: func2.hello Runtime: python2.7 Type: AWS::Serverless::Function Transform: AWS::Serverless-2016-10-31
S3上のアーティファクトを確認すると、特定のコードのみがデプロイメントパッケージに含まれていることが確認できます。
$ aws s3 cp s3://<_YOUR_S3_BUCKET_>/bfd5cbc1d484c4901175e3d180159bfe - | bsdtar -tvf - -rwxrwxrwx 0 0 0 54 Feb 12 17:03 func1.py $ aws s3 cp s3://<_YOUR_S3_BUCKET_>/3e0ecc95dc3ada08a3d0bfbbe72aebab - | bsdtar -tvf - -rwxrwxrwx 0 0 0 54 Feb 12 17:03 func2.py
まとめ
いかがだったでしょうか。
AWS SAMのCodeUriプロパティとデプロイメントパッケージの関係についてご紹介しました。この機能は昨年11月頃に発表された比較的新しい機能です。そのためか、まだWeb上の情報が少ない印象があります。今後もブログで検証した内容などをご紹介できればと思っています。
本エントリがみなさんの参考になれば幸いに思います。